Path: blob/master/src/packages/next/pages/api/youtube-thumbnail/[id].ts
14425 views
/*1* This file is part of CoCalc: Copyright © 2026 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// Server-side proxy for YouTube video thumbnails. The click-to-load video6// gate (components/videos.tsx) uses this so visitors see a still image7// without their browser contacting i.ytimg.com / Google before they have8// explicitly consented to YouTube embeds.9//10// Thumbnails are effectively immutable per id, so we cache aggressively at11// the CDN / browser layer. We do not buffer in-process — Next.js / the12// upstream proxy handles concurrency fine, and adding a per-process LRU13// here would complicate cold start without measurable wins.1415import type { NextApiRequest, NextApiResponse } from "next";1617// YouTube video ids are 11 chars of [A-Za-z0-9_-]. Be slightly lenient18// (6-20) so a future id-length change doesn't silently break the page,19// while still rejecting obvious junk that would just produce a 404.20const ID_RE = /^[A-Za-z0-9_-]{6,20}$/;2122// hqdefault is 480x360 with letterboxing for 16:9 sources — good enough23// for the carousel at 672px wide and always present. mqdefault is the24// fallback for the rare id where hqdefault is missing.25const VARIANTS = ["hqdefault", "mqdefault"] as const;2627export default async function handler(28req: NextApiRequest,29res: NextApiResponse,30): Promise<void> {31const id = String(req.query.id ?? "");32if (!ID_RE.test(id)) {33res.status(400).send("invalid id");34return;35}36for (const variant of VARIANTS) {37const url = `https://i.ytimg.com/vi/${id}/${variant}.jpg`;38let upstream: Response;39try {40upstream = await fetch(url);41} catch (err) {42// Network blip; try next variant before giving up. We don't log43// every failure — a misconfigured firewall would otherwise flood44// the hub logs with one entry per page view.45continue;46}47if (!upstream.ok) continue;48const buf = Buffer.from(await upstream.arrayBuffer());49res.setHeader("content-type", "image/jpeg");50res.setHeader(51"cache-control",52"public, max-age=86400, s-maxage=604800, stale-while-revalidate=604800",53);54res.status(200).send(buf);55return;56}57res.status(404).send("not found");58}596061